/*
 * Copyright [2022] [https://www.xiaonuo.vip]
 *
 * Snowy采用APACHE LICENSE 2.0开源协议，您在使用过程中，需要注意以下几点：
 *
 * 1.请不要删除和修改根目录下的LICENSE文件。
 * 2.请不要删除和修改Snowy源码头部的版权声明。
 * 3.本项目代码可免费商业使用，商业使用请保留源码和相关描述文件的项目出处，作者声明等。
 * 4.分发源码时候，请注明软件出处 https://www.xiaonuo.vip
 * 5.不可二次分发开源参与同类竞品，如有想法可联系团队xiaonuobase@qq.com商议合作。
 * 6.若您的项目无法满足以上几点，需要更多功能代码，获取Snowy商业授权许可，请在官网购买授权，地址为 https://www.xiaonuo.vip
 */
package vip.xiaonuo.auth.modular.login.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import vip.xiaonuo.auth.api.SaBaseLoginUserApi;
import vip.xiaonuo.auth.core.pojo.SaBaseClientLoginUser;
import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
import vip.xiaonuo.auth.core.util.StpClientUtil;
import vip.xiaonuo.auth.modular.login.enums.AuthDeviceTypeEnum;
import vip.xiaonuo.auth.modular.login.service.AuthService;
import vip.xiaonuo.auth.modular.login.service.SSOService;
import vip.xiaonuo.common.cache.CommonCacheOperator;
import vip.xiaonuo.dev.api.DevConfigApi;
import vip.xiaonuo.sys.api.SysUserApi;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class SSOServiceImpl implements SSOService {
    public static final String sso_verify_key_pattern = "sso:verifier:%s";
    public static final String sso_token_pc_key_pattern = "sso:token:pc:%s";
    public static final String sso_token_h5_key_pattern = "sso:token:h5:%s";

    private String backend_base_url;
    private String sso_base_url;
    private String sso_client_id;
    private String sso_client_secret;
    private String sso_redirect_url;
    private String pc_frontend_base_url;
    private String h5_frontend_base_url;
    @Resource
    private CommonCacheOperator commonCacheOperator;
    @Resource
    private AuthService authService;
    @Resource(name = "loginUserApi")
    private SaBaseLoginUserApi loginUserApi;
    @Resource(name = "clientLoginUserApi")
    private SaBaseLoginUserApi clientLoginUserApi;
    @Resource
    private DevConfigApi devConfigApi;
    @Resource
    private SysUserApi sysUserApi;

    @PostConstruct
    public void init() {
        backend_base_url = devConfigApi.getValueByKey("BACKEND_BASE_URL");
        sso_base_url = devConfigApi.getValueByKey("SSO_BASE_URL");
        sso_client_id = devConfigApi.getValueByKey("SSO_CLIENT_ID");
        sso_client_secret = devConfigApi.getValueByKey("SSO_SECRET");
        sso_redirect_url = devConfigApi.getValueByKey("SSO_REDIRECT_URL");
        pc_frontend_base_url = devConfigApi.getValueByKey("PC_FRONTEND_BASE_URL");
        h5_frontend_base_url = devConfigApi.getValueByKey("H5_FRONTEND_BASE_URL");
    }

    @Override
    public String authorize(String client) throws NoSuchAlgorithmException {
        StringBuilder sb = new StringBuilder(sso_base_url);
        sb.append("/connect/authorize?");
        sb.append("client_id=").append(sso_client_id);
        sb.append("&client_secret=").append(sso_client_secret);
        sb.append("&redirect_uri=").append(sso_redirect_url);
        sb.append("&response_type=").append("code");
        sb.append("&response_mode=").append("query");
        sb.append("&scope=").append("openid profile");
        String codeVerifier = getCodeVerifier();
        // put a kv into redis and the value is the code verifier.
        // and then use the key as a state value
        String state = UUID.randomUUID().toString();
        JSONObject ssoVerifyValue = new JSONObject();
        ssoVerifyValue.put("code_verifier", codeVerifier);
        ssoVerifyValue.put("client", client);
        commonCacheOperator.put(String.format(sso_verify_key_pattern, state), ssoVerifyValue.toJSONString(), 60 * 30);

        sb.append("&state=").append(state);
        String codeChallenge = getCodeChallenge(codeVerifier);
        sb.append("&code_challenge=").append(codeChallenge);
        sb.append("&code_challenge_method=").append("S256");
        log.info("调用SSO登录鉴权回调URL={}", sb.toString());
        return sb.toString();
    }

    /**
     * first: get the id_token and access_token and store them in redis
     * second: invoke SSO system getUserInfo endpoint to get userInfo
     * third: do internal log in process
     * forth: bind the relationship of external token and internal token by userId
     *
     * @param code
     * @param state
     * @return
     */
    @Override
    public String token(String code, String state) {
        Map<String, Object> param = new HashMap<>();
        param.put("client_id", sso_client_id);
        param.put("client_secret", sso_client_secret);
        param.put("redirect_uri", sso_redirect_url);
        param.put("grant_type", "authorization_code");
        param.put("code", code);
        Object ssoVerifierValue = commonCacheOperator.get(String.format(sso_verify_key_pattern, state));
        if (ssoVerifierValue == null) throw new IllegalArgumentException("无效的登录请求，请返回重试");
        JSONObject ssoVerifierObject = JSONObject.parseObject(ssoVerifierValue.toString());
        String codeVerifier = ssoVerifierObject.getString("code_verifier");
        String client = ssoVerifierObject.getString("client");
        param.put("code_verifier", codeVerifier);
        log.info("调用SSO接口获得Token,请求值={}", JSONObject.toJSONString(param));
        String externalTokenStr = HttpUtil.post(sso_base_url + "/connect/token", param);
        JSONObject externalToken = JSON.parseObject(externalTokenStr);
        log.info("调用SSO接口获得Token,返回值={}", externalToken.toJSONString());
        String accessToken = externalToken.getString("access_token");
        int expiresIn = externalToken.getIntValue("expires_in");
        String userInfoStr = "";
        try {// if get errors when getting user info, redirect to the login page.
            userInfoStr = getSSOUserInfo(accessToken);
            log.info("调用SSO接口获得用户基本信息,返回值={}", userInfoStr);
        } catch (Exception e) {
            if ("pc".equals(client)) {
                return String.format("%s/login", pc_frontend_base_url);
            } else {
                return String.format("%s/pages/common/login/index", h5_frontend_base_url);
            }
        }
        JSONObject userInfoJson = JSONObject.parseObject(userInfoStr);
        String name = userInfoJson.getString("name");
        String displayName = userInfoJson.getString("display_name");
        String email = userInfoJson.getString("email");
        String emp_id = userInfoJson.getString("emp_id"); // user no
        String type = userInfoJson.getString("type"); // 接口1.1参数中profile对应返回参数，用户账号类型：staff,student,project

        int userType = 3;// 默认外部; '用户类型 1学生 2老师 3外部'
        if (StringUtils.isNotEmpty(type)) {
            if ("staff".equalsIgnoreCase(type)) userType = 2;
            if ("student".equalsIgnoreCase(type)) userType = 1;
        }
        //sign up
        if ("PC".equalsIgnoreCase(client)) {
            SaBaseLoginUser loginUser = loginUserApi.getUserByAccount(name);
            if (loginUser == null) { // if the user doesn't exist. insert one.
                sysUserApi.createNewUser(name, displayName, email, emp_id, userType); // add as a external user
                loginUser = loginUserApi.getUserByAccount(name); // fetch again.
            }
            String internalToken = authService.execLoginB(loginUser, AuthDeviceTypeEnum.PC.getValue());
            // bind relationship of tokens.
            commonCacheOperator.put(String.format(sso_token_pc_key_pattern, loginUser.getId()), externalTokenStr, expiresIn);
            return String.format("%s/login?token=%s", pc_frontend_base_url, internalToken);
        } else if ("h5".equalsIgnoreCase(client)) {
            SaBaseClientLoginUser loginUser = clientLoginUserApi.getClientUserByAccount(name);
            if (loginUser == null) {
                sysUserApi.createNewUser(name, displayName, email, emp_id, userType); // add as a external user
                loginUser = clientLoginUserApi.getClientUserByAccount(name);
            }
            String internalToken = authService.execLoginC(loginUser, AuthDeviceTypeEnum.H5.getValue());
            commonCacheOperator.put(String.format(sso_token_h5_key_pattern, loginUser.getId()), externalTokenStr, expiresIn);
            return String.format("%s/pages/common/home/index?token=%s", h5_frontend_base_url, internalToken);
        }
        return pc_frontend_base_url;
    }


    private String getSSOUserInfo(String token) {
        Map<String, String> param = new HashMap<>();
        param.put("Authorization", "Bearer " + token);
        HttpResponse httpResponse = HttpRequest.post(sso_base_url + "/connect/userinfo").addHeaders(param).execute();
        if (!httpResponse.isOk()) throw new IllegalArgumentException("单点登录发生错误");
        return httpResponse.body();
    }

    /**
     * if the sso token exists then return the sso logout endpoint with params, otherwise return the login page url.
     *
     * @return
     */
    @Override
    public String getSsoLogout(String client) throws UnsupportedEncodingException {
        if ("pc".equalsIgnoreCase(client)) {
            String userId = StpUtil.getLoginIdAsString();
            return doSSOLogout(sso_token_pc_key_pattern, userId);
        }
        String userId = StpClientUtil.getLoginIdAsString();
        return doSSOLogout(sso_token_h5_key_pattern, userId);
    }

    private String doSSOLogout(String tokenKey, String userId) throws UnsupportedEncodingException {
        Object tokenBeanObj = commonCacheOperator.get(String.format(tokenKey, userId));
        if (tokenBeanObj == null) return null; // if there's no necessary to do SSO logout, return null
        JSONObject externalToken = JSON.parseObject(tokenBeanObj.toString());
        return String.format(
                "%s/connect/endsession?id_token_hint=%s&post_logout_redirect_uri=%s",
                sso_base_url,
                externalToken.getString("id_token"),
                URLEncoder.encode(backend_base_url + "/sso/logout", "utf-8"));
    }

    private String getCodeVerifier() {
        SecureRandom sr = new SecureRandom();
        byte[] code = new byte[32];
        sr.nextBytes(code);
        return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(code);
    }

    private String getCodeChallenge(String verifier) throws NoSuchAlgorithmException {
        byte[] bytes = verifier.getBytes(StandardCharsets.US_ASCII);
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(bytes, 0, bytes.length);
        byte[] digest = md.digest();
        return Base64.encodeBase64URLSafeString(digest);
    }

}
